package com.gitee.fanx.sample.otpauth.common;

import com.eatthepath.otp.TimeBasedOneTimePasswordGenerator;
import org.apache.commons.codec.binary.Base32;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;

/**
 * @author fanhang
 */
@Component
public class TotpAuthenticator {
	private Logger log = LoggerFactory.getLogger(TotpAuthenticator.class);

	private final String issuer = "ppp";

	private TimeBasedOneTimePasswordGenerator totp;

	public TotpAuthenticator() throws NoSuchAlgorithmException {
		totp = new TimeBasedOneTimePasswordGenerator();
	}

	public String generateSecret(String phone) {
		return phone + "@" + issuer;
	}

	public String generateUri(String phone) throws IOException {
		return new AuthUri(phone, this.generateSecret(phone), issuer).toUriString();
	}

	public String generateUri(String phone, String secret, String issuer) throws IOException {
		return new AuthUri(phone, secret, issuer).toUriString();
	}

	public int validCode(String secret, int code) {
		try {
			Key key = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "SHA1");
			final Instant now = Instant.now();
			final Instant early = now.minus(totp.getTimeStep());
			return this.valid(code, key, now, early);
		} catch (InvalidKeyException e) {
			throw new RuntimeException(e.getMessage());
		}
	}

	private int valid(int code, Key key, Instant... times) throws InvalidKeyException {
		if (times == null || times.length == 0) {
			return -2;
		}
		for (int i = 0; i < times.length; i++) {
			int generateCode = totp.generateOneTimePassword(key, times[i]);
			if (generateCode == code) {
				return i;
			}
		}
		return -1;
	}

	public static class AuthUri {
		private static final Base32 BASE_32 = new Base32();

		private final String type = "totp";
		private final String label;
		private final String secret;
		private final String issuer;
		private final String algorithm = "SHA1";

		/**
		 * Force use of builder to create instances.
		 */
		public AuthUri(String label, String secret, String issuer) {
			this.label = label;
			this.secret = secret;
			this.issuer = issuer;
		}

		public String getType() {
			return type;
		}

		public String getLabel() {
			return label;
		}

		public String getSecret() {
			return secret;
		}

		public String getIssuer() {
			return issuer;
		}

		public String getAlgorithm() {
			return algorithm;
		}

		/**
		 * @return The URI/message to encode into the QR image, in the format specified here:
		 * https://github.com/google/google-authenticator/wiki/Key-Uri-Format
		 */
		public String toUriString() throws UnsupportedEncodingException {
			StringBuilder uri = new StringBuilder(64);
			uri.append("otpauth://").append(type).append("/").append(label).append("?secret=").append(BASE_32.encodeAsString(secret.getBytes(StandardCharsets.UTF_8))).append("&algorithm=").append(algorithm);
			if (StringUtils.hasLength(issuer)) {
				uri.append("&issuer=").append(uriEncode(issuer));
			}
			return uri.toString();
		}

		private String uriEncode(String text) throws UnsupportedEncodingException {
			return text == null ? "" : URLEncoder.encode(text, StandardCharsets.UTF_8.toString()).replaceAll("\\+", "%20");
		}
	}

}
